package mongo

import (
	"context"
	"errors"
	"go2/log"
	"strings"
	"time"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

const (
	ActionFind             = 1
	ActionFindOne          = 2
	ActionFindOneAndUpdate = 3
	ActionFindOneAndDelete = 4
	ActionDistinct         = 5
	ActionCountDocuments   = 6
	ActionInsertMany       = 7
	ActionInsertOne        = 8
	ActionReplaceOne       = 9
	ActionDeleteOne        = 10
	ActionDeleteMany       = 11
	ActionUpdateMany       = 12
	ActionUpdateOne        = 13

	maxRetries = 30
	baseDelay  = 500 * time.Millisecond
)

var (
	ErrInvalidParams      = errors.New("invalid parameters: db_name, table_name and action are required")
	ErrCollectionNotFound = errors.New("collection not found")
)

type MongoResult struct {
	Data any
	Err  error
}

func castOptions[T any](opts []any) []*T {
	result := make([]*T, 0, len(opts))
	for _, opt := range opts {
		if typedOpt, ok := opt.(*T); ok {
			result = append(result, typedOpt)
		}
	}
	return result
}

func MongoShell(dbName, tableName string, action int, filter, arg1 any, opts ...any) *MongoResult {
	if dbName == "" || tableName == "" || action == 0 {
		log.Errorf("DoShell db_name:%s table_name:%s action:%d", dbName, tableName, action)
		return &MongoResult{Data: nil, Err: ErrInvalidParams}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	for retry := 0; retry < maxRetries; retry++ {
		coll := GetMGClient().Database(dbName).Collection(tableName)
		if coll == nil {
			log.Errorf("ERROR: Mongo DoShell db_name:%s table_name:%s", dbName, tableName)
			if retry < maxRetries-1 {
				time.Sleep(baseDelay * time.Duration(retry+1))
				continue
			}
			return &MongoResult{Data: nil, Err: ErrCollectionNotFound}
		}

		result := executeMongoAction(ctx, coll, action, filter, arg1, opts)
		if result.Err == nil {
			return result
		}

		if !shouldRetry(result.Err) {
			return result
		}

		if retry < maxRetries-1 {
			delay := baseDelay * time.Duration(retry+1)
			log.Errorf("Retrying mongo operation (attempt %d/%d) after %v: %v", retry+1, maxRetries, delay, result.Err)
			time.Sleep(delay)
		}
	}

	return &MongoResult{Data: nil, Err: errors.New("max retries exceeded")}
}

func shouldRetry(err error) bool {
	if mongo.IsTimeout(err) || mongo.IsNetworkError(err) {
		return true
	}
	if mongo.IsDuplicateKeyError(err) {
		return false
	}
	if strings.Contains(err.Error(), "no documents in result") || strings.Contains(err.Error(), "document is nil") {
		return false
	}
	return false
}

func executeMongoAction(ctx context.Context, coll *mongo.Collection, action int, filter any, arg1 any, opts []any) *MongoResult {
	switch action {
	case ActionFind:
		cursor, err := coll.Find(ctx, filter, castOptions[options.FindOptions](opts)...)
		return &MongoResult{Data: cursor, Err: err}

	case ActionFindOne:
		result := coll.FindOne(ctx, filter, castOptions[options.FindOneOptions](opts)...)
		return &MongoResult{Data: result, Err: result.Err()}

	case ActionFindOneAndDelete:
		result := coll.FindOneAndDelete(ctx, filter, castOptions[options.FindOneAndDeleteOptions](opts)...)
		return &MongoResult{Data: result, Err: result.Err()}

	case ActionFindOneAndUpdate:
		result := coll.FindOneAndUpdate(ctx, filter, arg1, castOptions[options.FindOneAndUpdateOptions](opts)...)
		return &MongoResult{Data: result, Err: result.Err()}

	case ActionDistinct:
		fieldName, ok := arg1.(string)
		if !ok {
			return &MongoResult{Data: nil, Err: errors.New("arg1 must be string for Distinct operation")}
		}
		result, err := coll.Distinct(ctx, fieldName, filter, castOptions[options.DistinctOptions](opts)...)
		return &MongoResult{Data: result, Err: err}

	case ActionCountDocuments:
		count, err := coll.CountDocuments(ctx, filter, castOptions[options.CountOptions](opts)...)
		return &MongoResult{Data: count, Err: err}

	case ActionInsertMany:
		docs, ok := arg1.([]any)
		if !ok {
			return &MongoResult{Data: nil, Err: errors.New("arg1 must be []any for InsertMany operation")}
		}
		result, err := coll.InsertMany(ctx, docs, castOptions[options.InsertManyOptions](opts)...)
		return &MongoResult{Data: result, Err: err}

	case ActionInsertOne:
		result, err := coll.InsertOne(ctx, arg1, castOptions[options.InsertOneOptions](opts)...)
		return &MongoResult{Data: result, Err: err}

	case ActionReplaceOne:
		result, err := coll.ReplaceOne(ctx, filter, arg1, castOptions[options.ReplaceOptions](opts)...)
		return &MongoResult{Data: result, Err: err}

	case ActionDeleteOne:
		result, err := coll.DeleteOne(ctx, filter, castOptions[options.DeleteOptions](opts)...)
		return &MongoResult{Data: result, Err: err}

	case ActionDeleteMany:
		result, err := coll.DeleteMany(ctx, filter, castOptions[options.DeleteOptions](opts)...)
		return &MongoResult{Data: result, Err: err}

	case ActionUpdateMany:
		result, err := coll.UpdateMany(ctx, filter, arg1, castOptions[options.UpdateOptions](opts)...)
		return &MongoResult{Data: result, Err: err}

	case ActionUpdateOne:
		result, err := coll.UpdateOne(ctx, filter, arg1, castOptions[options.UpdateOptions](opts)...)
		return &MongoResult{Data: result, Err: err}

	default:
		return &MongoResult{Data: nil, Err: errors.New("unknown action")}
	}
}
